/*
 * Copyright 2008 Carnegie Mellon University
 * Licensed under the Apache License, Version 2.0 (the "License"); 
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *  
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, 
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied. See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package edu.cmu.lti.sepia.aclia;

import java.io.IOException;
import java.io.InputStream;
import java.util.HashSet;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import edu.cmu.lti.sepia.data.Lang;
import edu.cmu.lti.sepia.data.resource.RunType;
import edu.cmu.lti.sepia.infra.run.RunXmlFacade;
import edu.cmu.lti.sepia.infra.run.model.AnswerCandidate;
import edu.cmu.lti.sepia.infra.run.model.Document;
import edu.cmu.lti.sepia.infra.run.model.QuestionAnalysisResult;
import edu.cmu.lti.sepia.infra.run.model.QuestionAnalysisResultSet;
import edu.cmu.lti.sepia.infra.run.model.RetrievalResult;
import edu.cmu.lti.sepia.infra.run.model.RetrievalResultSet;
import edu.cmu.lti.sepia.infra.run.model.SelectionResult;
import edu.cmu.lti.sepia.infra.run.model.SelectionResultSet;
import edu.cmu.lti.sepia.task.validation.Validator;
import edu.cmu.lti.sepia.util.FormatUtil;

public class FormatValidator implements Validator {

  private final static int MIN_DESCRIPTION_LENGTH = 10;
  
  private final String RUNID = "RUNID";
  private final String DESCRIPTION = "DESCRIPTION";
  
  private final static Pattern pRunId = Pattern.compile("^(.+?)\\-(EN|JA|CS|CT)\\-(JA|CS|CT)\\-([0-9]{2})\\-([TDN]{1,2})$");
  private final static Pattern pJADocId = Pattern.compile("^JA-0[2-5][01][0-9][0-3][0-9][0-9]{3}$");
  private final static Pattern pCSDocId = Pattern.compile("^XIN_CMN_200[2-5][01][0-9][0-3][0-9]\\.[0-9]{4}$");
  private final static Pattern pCTDocId = Pattern.compile("^(edn|udn|mhn|stn|ude)_xxx_200[2-5][01][0-9][0-3][0-9]_[0-9]{7,9}$");
  
  private String trgLang;
  private String report;
  private String runid;
  private String description;
  
  private final static String NO_RESULT = "<strong>ERROR: No result contained in the submitted file. Check if the filetype is correct, the xml format is correct, and the encoding is in UTF-8.</strong><br>\n"; 
  private final static String SHORT_DESCRIPTION = "<strong>ERROR: Description in metadata is not found or too short.</strong> Write it so that other anyone can quickly understand the characteristic of the run.<br>";
  
  /**
   * Validates if the correct ACLIA format is given  
   * @param is
   * @param filetype
   * @return report
   *   Make sure to include a string "ERROR" in the report when validation fails 
   * @throws IOException
   */
  public void runValidation(InputStream is, String filetype) {
    try {
      if (RunType.qa.toString().equalsIgnoreCase(filetype)) {
        validateQuestionAnalysisSubmission(is);
      } else if (RunType.ir4qa.toString().equalsIgnoreCase(filetype)) {
        validateIR4QASubmission(is);
      } else if (RunType.cclqa.toString().equalsIgnoreCase(filetype)) {
        validateCLQASubmission(is);
      } else {
        runid = "";
        description = "";
        report = "";
      }
    } catch (Exception e) {
      e.printStackTrace();
    }
  }

  public void validateQuestionAnalysisSubmission(InputStream is) throws Exception {
    QuestionAnalysisResultSet resultSet = RunXmlFacade.deserializeQuestionAnalysisResultSet(is);
    if (resultSet==null) {
      this.report = NO_RESULT;
      return;
    }
    runid = resultSet.getMetadata().get(RUNID);
    description = resultSet.getMetadata().get(DESCRIPTION);
    
    StringBuilder sb = new StringBuilder();
    RunId runidObj = validateRunId(runid);
    this.trgLang = runidObj.trgLang;
    if (runidObj.report.length()>0) {
      report = runidObj.report;
      return;
    }
    
    if (description==null || description.length()<MIN_DESCRIPTION_LENGTH) {
      sb.append( SHORT_DESCRIPTION );
    }
    
    int n = resultSet.size();
    int sumAtypes = 0;
    int sumKeyterms = 0;
    for (String key : resultSet.keySet()) {
      QuestionAnalysisResult result = resultSet.get(key);
      if ( result != null ) {
        sumAtypes += result.getAnswerTypes().size();
        sumKeyterms += result.getKeyterms().size();
      }
    }
    if (n==0||(sumAtypes+sumKeyterms)==0) {
      sb.append( NO_RESULT );
    } else {
      sb.append( "Statistics:<br>" );
      sb.append( "# of submitted topics = "+n+"<br>" );
      sb.append( "# of answer type / topic = "+FormatUtil.nf2(sumAtypes/n)+"<br>" );
      sb.append( "# of keyterms / topic = "+FormatUtil.nf2(sumKeyterms/n)+"<br>" );
    }
    this.report = sb.toString();
  }
  
  public void validateIR4QASubmission(InputStream is) throws Exception {
    RetrievalResultSet resultSet = RunXmlFacade.deserializeRetrievalResultSet(is);
    if (resultSet==null) {
      this.report = NO_RESULT;
      return;
    }
    runid = resultSet.getMetadata().get(RUNID);
    description = resultSet.getMetadata().get(DESCRIPTION);

    StringBuilder sb = new StringBuilder();
    RunId runidObj = validateRunId(runid);
    this.trgLang = runidObj.trgLang;
    if (runidObj.report.length()>0) {
      report = runidObj.report;
      return;
    }

    if (description==null || description.length()<MIN_DESCRIPTION_LENGTH) {
      sb.append( SHORT_DESCRIPTION );
    }
    
    int sum = 0;

    String lang = runidObj.trgLang;
    sb.append( verifyDocIds( lang, resultSet ) );
    
    for (String key : resultSet.keySet()) {
      RetrievalResult documents = resultSet.get(key);
      sum += documents.size();
    }

    int n = resultSet.keySet().size();
    if (n==0 || sum==0) {
      sb.append(NO_RESULT);
    } else {
      double avg = (double)sum / (double)n;
      sb.append( "Statistics:<br>" );
      sb.append( "# of submitted topics = "+n+"<br>" );
      sb.append( "# of docs / topic = "+FormatUtil.nf2(avg)+"<br>" );
    }
    report = sb.toString();
  }
  
  public void validateCLQASubmission(InputStream is) throws Exception {
    SelectionResultSet resultSet = RunXmlFacade.deserializeSelectionResultSet(is);
    if (resultSet==null) {
      this.report = NO_RESULT;
      return;
    }
    runid = resultSet.getMetadata().get(RUNID);
    description = resultSet.getMetadata().get(DESCRIPTION);
    
    StringBuilder sb = new StringBuilder();

    RunId runidObj = validateRunId(runid);
    this.trgLang = runidObj.trgLang;
    if (runidObj.report.length()>0) {
      report = runidObj.report;
      return;
    }

    if (description==null || description.length()<MIN_DESCRIPTION_LENGTH) {
      sb.append( SHORT_DESCRIPTION );
    }
    
    int sum = 0;

    String lang = runidObj.trgLang;
    sb.append( verifyAnswers( lang, resultSet ) );
    
    for (String key : resultSet.keySet()) {
      sum += resultSet.get(key).size();
    }
    
    int n = resultSet.keySet().size();
    if (n==0 || sum==0) {
      sb.append(NO_RESULT);
    } else {
      double avg = (double)sum / (double)n;
      sb.append( "Statistics:<br>" );
      sb.append( "# of submitted topics = "+n+"<br>" );
      sb.append( "# of answer candidates / topic = "+FormatUtil.nf2(avg)+"<br>" );
    }
    report = sb.toString();
  }
  
  private RunId validateRunId(String runid) {
    RunId runId = new RunId();
    if (runid==null||runid.length()==0) {
      runId.report = "<strong>ERROR: Run ID is not available in the metadata.</strong><br>";
    }
    Matcher mRunId = pRunId.matcher(runid);
    if (mRunId.find()) {
      runId.trgLang = mRunId.group(3);
    } else {
      runId.report = "<strong>ERROR: Run ID \""+runid+"\" does not follow the format.</strong><br>";
    }
    return runId;
  }
  
  class RunId {
    public String report = "";
    public String trgLang = "";
  }
  
  private String verifyDocIds( String langString, RetrievalResultSet resultSet ) {
    StringBuilder duplicationReport = new StringBuilder();
    StringBuilder formatReport = new StringBuilder();
    StringBuilder tooManyReport = new StringBuilder();
    Pattern p = null;
    Lang lang = Lang.valueOf(langString.toUpperCase());
    if (lang.equals(Lang.CS)) {
      p = pCSDocId;
    } else if (lang.equals(Lang.CT)) {
      p = pCTDocId;
    } else if (lang.equals(Lang.JA)) {
      p = pJADocId;
    }

    int counter = 0;
    OUTER :for (String key : resultSet.keySet()) {
      RetrievalResult documents = resultSet.get(key);
      Set<String> history = new HashSet<String>(documents.size());
      if (documents.size()>1000) {
        tooManyReport.append("<li>"+key+" contains "+documents.size()+" documents</li>\n");
      }
      for (Document d : documents) {
        String docid = d.getDocid();
        if (history.contains(docid)) {
          duplicationReport.append("<li>"+key+" : "+docid+"</li>\n");
        }
        history.add(docid);
        if ( !p.matcher(docid).find() ) {
          formatReport.append("<li>"+key+" : "+docid+"</li>\n");
          if ( counter++>100 ) {
            formatReport.append("<li>: : :</li>\n");
            break OUTER;
          }
        }
      }
    }
    
    StringBuilder report = new StringBuilder();
    if (formatReport.length()>0) {
      report.append("<strong>ERROR: Following documents are invalid:</strong>\n<ul>\n"+formatReport+"</ul>\n");
    }
    if (duplicationReport.length()>0) {
      report.append("<strong>ERROR: Following documents are duplicated:</strong>\n<ul>\n"+duplicationReport+"</ul>\n");
    }
    if (tooManyReport.length()>0) {
      report.append("<strong>WARNING: Too many documents returned.</strong>\n<ul>\n"+tooManyReport+"</ul>\n");
    }
    return report.toString();
  }
  
  private String verifyAnswers( String lang, SelectionResultSet resultSet ) {
    StringBuilder duplicationReport = new StringBuilder();
    StringBuilder formatReport = new StringBuilder();
    StringBuilder tooManyReport = new StringBuilder();
    Pattern p = null;
    if (lang.equalsIgnoreCase("CS")) {
      p = pCSDocId;
    } else if (lang.equalsIgnoreCase("CT")) {
      p = pCTDocId;
    } else if (lang.equalsIgnoreCase("JA")) {
      p = pJADocId;
    }
    
    int counter = 0;
    OUTER :for (String key : resultSet.keySet()) {
      SelectionResult answers = resultSet.get(key);
      Set<String> history = new HashSet<String>(answers.size());
      if (answers.size()>30) {
        tooManyReport.append("<li>"+key+" contains "+answers.size()+" documents</li>\n");
      }
      for (AnswerCandidate ac : answers) {
        String docid = ac.getDocid();
        String answer = ac.getText();
        if (history.contains(answer)) {
          duplicationReport.append("<li>"+key+" : "+docid+"</li>\n");
        }
        history.add(answer);
        if ( !p.matcher(docid).find() ) {
          formatReport.append("<li>"+key+" : "+docid+"</li>\n");
          if ( counter++>100 ) {
            formatReport.append("<li>: : :</li>\n");
            break OUTER;
          }
        }
      }
    }
    
    StringBuilder report = new StringBuilder();
    if (formatReport.length()>0) {
      report.append("<strong>ERROR: Following documents are invalid:</strong>\n<ul>\n"+formatReport+"</ul>\n");
    }
    if (duplicationReport.length()>0) {
      report.append("<strong>WARNING: Following answers are duplicated:</strong>\n<ul>\n"+duplicationReport+"</ul>\n");
    }
    if (tooManyReport.length()>0) {
      report.append("<strong>WARNING: Too many answers returned.</strong>\n<ul>\n"+tooManyReport+"</ul>\n");
    }
    return report.toString();
  }
  
  /**
   * @return the report
   */
  public String getReport() {
    return report;
  }

  /**
   * @return the runid
   */
  public String getRunid() {
    return runid;
  }

  /**
   * @return the description
   */
  public String getDescription() {
    return description;
  }

  /**
   * @return the trgLang
   */
  public String getTrgLang() {
    return trgLang;
  }
  
}
